[t:/]$ 지식_

avx2, simd와 align

2017/02/09

avx2, simd 쪼렙임.

gcc를 그냥 빌드 하면 simd로 빌드된다. 따라서 128비트짜리 xmm 레지스터를 사용한다. gcc에 -march=haswell을 넣고 빌드하면 avx2로 빌드된다. 256비트짜리 ymm 레지스터를 사용할 수 있다.

... 하 글쓰다 날렸다 ... 소스 생략하고 요약하면 이렇다.

  1. simd로 빌드하여 xmm 레지스터를 이용하면 128비트 초과시 3쿠션을 태운다.
  2. avx2로 빌드하여 ymm 레지스터를 이용하면 한 큐에 퉁친다.
  3. 명령어 갯수만 줄어드는 것이 아니라 메모리 적재까지 줄어든다.
  4. ymm레지스터를 사용할 때에도 256비트를 초과하면 메모리 적재를 한 큐에 하지 못한다. (gcc 5.4.0 우분투 빌드, 최적화 없을 때 확인해 봄.)
  5. 따라서 align에 맞춰서 포인터 연산으로 전환하면 다음과 같이 최적화 된다.

.

    movq    $a, -40(%rbp)
    movq    $b, -32(%rbp)
    movq    $c, -24(%rbp)
    movq    -40(%rbp), %rax
    vmovdqa (%rax), %ymm1
    movq    -32(%rbp), %rax
    vmovdqa (%rax), %ymm0
    vpaddd  %ymm0, %ymm1, %ymm0
    movq    -24(%rbp), %rax
    vmovdqa %ymm0, (%rax)
    addq    $32, -40(%rbp)
    addq    $32, -32(%rbp)
    addq    $32, -24(%rbp)
    movq    -40(%rbp), %rax
    vmovdqa (%rax), %ymm1
    movq    -32(%rbp), %rax
    vmovdqa (%rax), %ymm0
    vpaddd  %ymm0, %ymm1, %ymm0
    movq    -24(%rbp), %rax
    vmovdqa %ymm0, (%rax)

. . .

원본 c는 이렇다.

#include <stdio.h>

typedef int v4si __attribute__ ((vector_size (32)));

int a[] = {1,2,3,4,5,6,7,8, 2,3,4,4,5,6,7,8};
int b[] = {2,3,4,4,5,6,7,8, 2,3,4,4,5,6,7,8};
int c[16];

int main(int argc, char *argv[])
{
     v4si *ap, *bp, *cp;

     ap = a;
     bp = b;
     cp = c;
     *cp = *ap + *bp;

     ap++, bp++, cp++;
     *cp = *ap + *bp;

     printf("%d\n", c[0]);

}

...

얼라인을 맞추지 않고 그냥 날 계산을 때리면 메모리 복사를 한다고 낑낑댄다..

    movq    a(%rip), %rax
    movq    %rax, -304(%rbp)
    movq    a+8(%rip), %rax
    movq    %rax, -296(%rbp)
    movq    a+16(%rip), %rax
    movq    %rax, -288(%rbp)
    movq    a+24(%rip), %rax
    movq    %rax, -280(%rbp)
    movq    a+32(%rip), %rax
    movq    %rax, -272(%rbp)
    movq    a+40(%rip), %rax
    movq    %rax, -264(%rbp)
    movq    a+48(%rip), %rax
    movq    %rax, -256(%rbp)
    movq    a+56(%rip), %rax
    movq    %rax, -248(%rbp)
    movq    b(%rip), %rax
    movq    %rax, -240(%rbp)
    movq    b+8(%rip), %rax
    movq    %rax, -232(%rbp)
    movq    b+16(%rip), %rax
    movq    %rax, -224(%rbp)
    movq    b+24(%rip), %rax
    movq    %rax, -216(%rbp)
    movq    b+32(%rip), %rax
    movq    %rax, -208(%rbp)
    movq    b+40(%rip), %rax
    movq    %rax, -200(%rbp)
    movq    b+48(%rip), %rax
    movq    %rax, -192(%rbp)
    movq    b+56(%rip), %rax
    movq    %rax, -184(%rbp)
    vmovdqa -304(%rbp), %ymm1
    vmovdqa -240(%rbp), %ymm0
    vpaddd  %ymm0, %ymm1, %ymm1
    vmovdqa -272(%rbp), %ymm2
    vmovdqa -208(%rbp), %ymm0
    vpaddd  %ymm0, %ymm2, %ymm0
    vmovdqa %ymm1, -464(%rbp)
    vmovdqa %ymm0, -432(%rbp)
    movq    -464(%rbp), %rax
    movq    %rax, -176(%rbp)
    movq    -456(%rbp), %rax
    movq    %rax, -168(%rbp)
    movq    -448(%rbp), %rax
    movq    %rax, -160(%rbp)
    movq    -440(%rbp), %rax
    movq    %rax, -152(%rbp)
    movq    -432(%rbp), %rax
    movq    %rax, -144(%rbp)
    movq    -424(%rbp), %rax
    movq    %rax, -136(%rbp)
    movq    -416(%rbp), %rax
    movq    %rax, -128(%rbp)
    movq    -408(%rbp), %rax
    movq    %rax, -120(%rbp)
    movq    -176(%rbp), %rax
    movq    %rax, -368(%rbp)
    movq    -168(%rbp), %rax
    movq    %rax, -360(%rbp)
    movq    -160(%rbp), %rax
    movq    %rax, -352(%rbp)
    movq    -152(%rbp), %rax
    movq    %rax, -344(%rbp)
    movq    -144(%rbp), %rax
    movq    %rax, -336(%rbp)
    movq    -136(%rbp), %rax
    movq    %rax, -328(%rbp)
    movq    -128(%rbp), %rax
    movq    %rax, -320(%rbp)
    movq    -120(%rbp), %rax
    movq    %rax, -312(%rbp)

...

#include <stdio.h>

typedef int v4si __attribute__ ((vector_size (64)));

v4si a = {1,2,3,4,5,6,7,8, 2,3,4,4,5,6,7,8};
v4si b = {2,3,4,4,5,6,7,8, 2,3,4,4,5,6,7,8};

int main(int argc, char *argv[])
{
     printf("hello world\n");
     v4si c = a + b;
     printf("%d\n", c[0]);

}

O2를 붙여보니 내용은 좀 달라지지만 맥락은 같다. 결과적으로 보면, 그냥 C 프로그래밍 할 때와 마찬가지다. 포인터로 다루면 메모리 복사를 회피할 수 있다. 끗.

+추가 : 아이러니컬 하게도 캐시라인은 보통 64바이트다. 64바이트 이하의 전역 데이터를 다룰때 자칫하면 거짓공유에 빠질 수 있다. 전역 데이터의 멀티 코어 공유가 있다면 64바이트 패딩을 치고 avx2를 쓰는 쪽이 아무것도 하지 않은 것에 비해 수십 배는 빠를 것 같다.

+추가 : 마찬가지로 avx2를 이용하여 memcpy를 구현하면 64바이트 단위로 복사를 구현할 수 있다. 물론 현재의 memcpy는 최적화가 다방면으로 잘 되어 있다고 들었다. 일반적인 용도로 memcpy를 재구현할 필요는 없으나, 대규모 벡터 연산이 필요한 경우 avx2를 응용한 포인터 연산을 고려해 볼 수 있다.





공유하기













[t:/] is not "technology - root". dawnsea, rss